﻿#region MIT

//  /*The MIT License (MIT)
// 
//  Copyright 2016 lizs lizs4ever@163.com
//  
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//  
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//  
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//   * */

#endregion

using System;
using System.Net;
using System.Net.Sockets;

namespace mom
{
    /// <summary>
    ///     Tcp Server
    /// </summary>
    public sealed class Server
    {
        /// <summary>
        ///     config for tcp server
        /// </summary>
        public sealed class Config
        {
            /// <summary>
            ///     tcp binding ip
            /// </summary>
            public string Ip { get; set; }

            /// <summary>
            ///     tcp binding port
            /// </summary>
            public int Port { get; set; }

            /// <summary>
            ///     period for expired sessions' clearup(ms)
            /// </summary>
            public uint ClearupInterval { get; set; } = 60*1000;

            /// <summary>
            ///     event/msg dispatcher
            /// </summary>
            public IDispatcher Dispatcher { get; set; } = null;

            /// <summary>
            ///     backtrace
            /// </summary>
            public int Backlog { get; set; } = 10;
        }

        private ushort _sessionIdSeed;
        private readonly Config _config;

        /// <summary>
        ///     tcp binding ip
        /// </summary>
        public string Ip => _config.Ip;

        /// <summary>
        ///     tcp binding port
        /// </summary>
        public int Port => _config.Port;

        private IPAddress Address { get; }
        private EndPoint EndPoint { get; }

        /// <summary>Returns a string that represents the current object.</summary>
        /// <returns>A string that represents the current object.</returns>
        /// <filterpriority>2</filterpriority>
        public override string ToString()
        {
            return $"{Ip}:{Port}";
        }

        private SessionMgr SessionMgr { get; }
        private Socket _listener;
        private SocketAsyncEventArgs _acceptEvent;

        private readonly IDispatcher _dispatcher;
        private readonly Scheduler _scheduler = new Scheduler();

        /// <summary>
        ///     construct
        /// </summary>
        public Server(Config cfg)
        {
            _config = cfg;

            _dispatcher = new InternalDispatcher(cfg.Dispatcher ?? new DefaultDispatcher(),
                (id, reason) => { SessionMgr.RemoveSession(id, reason); });

            IPAddress address;
            if (!IPAddress.TryParse(Ip, out address))
                Logger.Ins.Fatal("Invalid ip [{0}]", Ip);

            Address = address;
            EndPoint = new IPEndPoint(Address, Port);

            SessionMgr = new SessionMgr();
        }

        private void OnError(string msg)
        {
            Logger.Ins.Error("{0}:{1}", this, msg);
        }

        /// <summary>
        ///     startup
        /// </summary>
        public void Start()
        {
            try
            {
                _listener = SocketExt.CreateTcpSocket(AddressFamily.InterNetwork);
                _listener.Bind(EndPoint);
                _listener.Listen(_config.Backlog);
            }
            catch (Exception e)
            {
                OnError($"Server start failed, detail {e.Message} : {e.StackTrace}");
                return;
            }

            _acceptEvent = new SocketAsyncEventArgs();
            _acceptEvent.Completed += OnAcceptCompleted;

            AcceptNext();

            // clear expired sessions
            _scheduler.Invoke(SessionMgr.ClearExpired, _config.ClearupInterval, _config.ClearupInterval);

            Logger.Ins.Info("Server started on {0}:{1}", Ip, Port);
        }

        /// <summary>
        ///     停止
        /// </summary>
        public void Stop()
        {
            SessionMgr.Stop();

            _listener.Close();
            _acceptEvent?.Dispose();

            Logger.Ins.Debug("Server stopped!");
        }

        private void OnAcceptCompleted(object sender, SocketAsyncEventArgs e)
        {
            ProcessAccept(e.AcceptSocket, e.SocketError);
        }

        private void ProcessAccept(Socket sock, SocketError error)
        {
            if (error != SocketError.Success)
            {
                Logger.Ins.Error($"ProcessAccept: {error}!");
            }
            else
            {
                var session = new Session(sock, ++_sessionIdSeed, _dispatcher);
                if (SessionMgr.AddSession(session))
                {
                    session.Start();
                }
            }

            AcceptNext();
        }

        private void AcceptNext()
        {
            _acceptEvent.AcceptSocket = null;

            try
            {
                if (!_listener.AcceptAsync(_acceptEvent))
                {
                    ProcessAccept(_acceptEvent.AcceptSocket, _acceptEvent.SocketError);
                }
            }
            catch (Exception e)
            {
                var msg = $"Accept failed, detail {e.Message} : {e.StackTrace}";
                OnError(msg);
                AcceptNext();
            }
        }
    }
}
